package com.github.zyf.ioaj.algorithm.swarmintelligence;

import com.github.zyf.ioaj.algorithm.Input;
import com.github.zyf.ioaj.algorithm.Output;
import com.github.zyf.ioaj.annotation.internal.Algorithm;
import com.github.zyf.ioaj.annotation.internal.BasicParam;
import com.github.zyf.ioaj.annotation.internal.TempParam;
import com.github.zyf.ioaj.constant.FileType;
import com.github.zyf.ioaj.constant.ParamType;
import com.github.zyf.ioaj.export.Report;
import com.github.zyf.ioaj.export.Tracker;

/**
 * <h1>Artificial Bee Colony</h1>
 * <p>人工蜂群算法</p>
 * <p>默认求解最小值，如果需要求解最大值，将目标函数乘以-1即可，最后得到的值再次乘以-1就是得到的最优值（自变量取值不用乘以-1）</p>
 * <p>需要计算则调用{@link #go()}</p>
 * <p>需要跟踪每次迭代则调用{@link #tracker(String, String, String, FileType)}</p>
 * <p>需要生成报告则调用{@link #report(String, String, String, FileType)}</p>
 * <p>该算法基本思想如下：</p>
 * <p>
 * 蜜蜂是一种群居生物，生物学家研究发现蜜蜂以跳舞的方式来交换蜜源信息。
 * 根据分工的不同，蜜蜂被分为三个工种：引领峰（雇佣蜂，EmployedBee）、跟随蜂（OnlookerBee）、侦察蜂（ScoutBee）。
 * 侦察蜂的职责是侦察蜜源（即蜜蜂的食物），一旦某一个侦察蜂找到蜜源后，实际上它的角色就切换为引领蜂了。
 * </p>
 * <p>
 * 找到蜜源后的引领峰以跳舞的方式向同伴传递自己找到食物的信息，
 * 这时候一些饥饿的、没有找到食物的蜜蜂就会沿着引领峰指明的方向去寻找食物，
 * 这些相信引领峰传递的信息的蜜蜂实际上就是跟随蜂。这实际上属于招募蜜蜂的行为。
 * </p>
 * <p>
 * 当引领峰寻找了多次蜜源后，发现再也找不到吃起来更香的蜜源后，
 * 引领峰说话的信服力会降低，于是跟随蜂也逐渐不相信引领峰传递的信息。
 * 引领峰认识到了一些不足之后，转变角色，做回侦察蜂。
 * 直到再次找到高质量的蜜源后，再做回引领峰，然后再带着大家继续寻找蜜源。
 * 这实际上属于放弃蜜源的行为。
 * </p>
 * <p>
 * 综上，蜂群实现群体智慧的最小搜索模型包括蜜源、引领蜂、跟随蜂和侦察蜂共4个组成要素，以及招募蜜蜂和放弃蜜源2种基本的行为。
 * 在算法中，招募蜜蜂的行为实际上就是保留当前较优解，放弃蜜源实际上就是更新解
 * </p>
 * <p>
 * 此外，因为侦察蜂与引领峰的角色是相互切换的，所以一开始需要假设当前这一部分蜜蜂全是引领峰。
 * </p>
 *
 * @author zyf
 */
@Algorithm
public class ABC extends SwarmIntelligenceAlgorithm {

    /* Control Parameters of ABC algorithm*/

    /**
     * 食物源数量，等于 蜂群大小/2
     */
    @BasicParam(type = ParamType.INT, desc = "食物源数量")
    private int FoodNumber;
    /**
     * 一个食物源未更新的最大限制次数
     */
    @BasicParam(type = ParamType.INT, desc = "未更新的最大限制次数")
    private int limit;

    /* Problem specific variables*/

    /**
     * 食物矩阵，行是食物源数量，列是自变量个数
     */
    @TempParam(type = ParamType.DOUBLE_ARRAY_2, desc = "食物矩阵")
    private double[][] Foods;
    /**
     * 目标函数向量
     */
    @TempParam(type = ParamType.DOUBLE_ARRAY_1, desc = "目标函数向量")
    private double[] f;
    /**
     * 适应度向量
     */
    @TempParam(type = ParamType.DOUBLE_ARRAY_1, desc = "适应度向量")
    private double[] fitness;
    /**
     * 一个食物源没有被更新的次数计数向量
     */
    @TempParam(type = ParamType.INT_ARRAY_1, desc = "计数向量")
    private int[] trial;
    /**
     * 一个食物源被选择的概率
     */
    @TempParam(type = ParamType.DOUBLE_ARRAY_1, desc = "选择概率向量")
    private double[] prob;

    /**
     * 一个新的食物源，位置公式：v_{ij}=x_{ij}+\phi_{ij}*(x_{kj}-x_{ij}) 得来
     */
    @TempParam(type = ParamType.DOUBLE_ARRAY_1, desc = "一个新的食物源")
    private double[] newFood;

    /**
     * 新食物源对应的目标函数值
     */
    @TempParam(type = ParamType.DOUBLE, desc = "新食物源对应的目标函数值")
    private double newFoodObjection;
    /**
     * 新食物源对应的适应度
     */
    @TempParam(type = ParamType.DOUBLE, desc = "新食物源对应的适应度")
    private double newFoodFitness;
    /**
     * 位置更新公式 v_{ij}=x_{ij}+\phi_{ij}*(x_{kj}-x_{ij}) 的 k
     */
    private int neighbour;
    /**
     * 位置更新公式 v_{ij}=x_{ij}+\phi_{ij}*(x_{kj}-x_{ij}) 的 j
     */
    private int param2change;

    /**
     * 最优解
     */
    @TempParam(type = ParamType.DOUBLE, desc = "当前最优解")
    private double optimizedValue;
    /**
     * 当前最优解自变量取值
     */
    @TempParam(type = ParamType.DOUBLE_ARRAY_1, desc = "当前最优解自变量取值")
    private double[] x;

    /**
     * [0,1) 的随机数
     */
    private double r;

    /**
     * 抽象算法构造函数，需要提供输入量，即提前初始化好{@link Input}对象
     *
     * @param input {@link Input}
     */
    public ABC(Input input) {
        super(input);
        setN(20);
        setIter(2500);
        init();
        // 先初始化一下最优值
        MemorizeBestSource();
    }

    /**
     * 初始化索引 index 处的值，包括了{@link #Foods},{@link #newFood},
     * {@link #f},{@link #fitness},{@link #trial}几个数组
     *
     * @param index 指定索引
     */
    void initInIndex(int index) {
        int j;
        for (j = 0; j < getInput().getVarNum(); j++) {
            Foods[index][j] = getInput().randomValueInRange(j);
            newFood[j] = Foods[index][j];
        }
        f[index] = getInput().getFunction().apply(newFood);
        fitness[index] = CalculateFitness(f[index]);
        trial[index] = 0;
    }

    /**
     * 初始化所有食物源以及相关参数
     */
    void init() {
        this.FoodNumber = getN() / 2;
        this.limit = 100;

        this.Foods = new double[FoodNumber][getInput().getVarNum()];
        this.f = new double[FoodNumber];
        this.fitness = new double[FoodNumber];
        this.trial = new int[FoodNumber];
        this.prob = new double[FoodNumber];
        this.newFood = new double[getInput().getVarNum()];
        this.x = new double[getInput().getVarNum()];

        int i;
        for (i = 0; i < FoodNumber; i++) {
            initInIndex(i);
        }
        optimizedValue = f[0];
        for (i = 0; i < getInput().getVarNum(); i++)
            x[i] = Foods[0][i];
    }

    /* 计算部分 */

    /**
     * 每次迭代的扫描逻辑
     */
    private void scan() {
        int i;
        for (i = 0; i < getIter(); i++) {
            SendEmployedBees();
            CalculateProbabilities();
            SendOnlookerBees();
            MemorizeBestSource();
            SendScoutBees();
        }
    }

    /**
     * 生成最优解
     */
    private void generateOptimization() {
        setOutput(new Output(optimizedValue, x));
    }

    /**
     * 运行并计算结果
     */
    @Override
    public void go() {
        init();
        int t;
        for (t = 0; t < getIter(); t++) {
            scan();
        }
        generateOptimization();
    }

    /**
     * 运行并计算结果（按迭代逐步跟踪，自带运行，无需手动），如果目的是计算出结果，请使用{@link #go()}
     *
     * @param author   作者
     * @param message  备注
     * @param title    标题
     * @param fileType 文件类型 {@link FileType}
     * @return {@link Tracker}
     */
    @Override
    public Tracker tracker(String author, String message, String title, FileType fileType) {
        init();
        int t;
        Tracker tracker = new Tracker(this, author, message, title, fileType);
        tracker.startTracker();
        for (t = 0; t < getIter(); t++) {
            scan();
            tracker.trackOne(t + 1);
        }
        generateOptimization();
        tracker.endTracker();
        return tracker;
    }

    /**
     * 生成报告（要先手动运行才能生成报告）
     *
     * @param author   作者
     * @param message  备注
     * @param title    标题
     * @param fileType 文件类型 {@link FileType}
     * @return {@link Report}
     */
    @Override
    public Report report(String author, String message, String title, FileType fileType) {
        return new Report(this, author, message, title, fileType);
    }

    /**
     * 更新雇佣蜂位置
     */
    void SendEmployedBees() {
        int i, j;
        int D = getInput().getVarNum();
        double[] lb = getInput().getLb();
        double[] ub = getInput().getUb();
        // 扫描所有食物源
        for (i = 0; i < FoodNumber; i++) {
            // 生成一个随机数，随机抽取一个食物源对应的自变量的位置，作为速度更新公式中的 j
            r = Math.random() * 32767.0 / (32767.0 + 1.0);
            param2change = (int) (r * D);

            // 生成一个随机数，随机抽取一个食物源，作为速度更新公式的 k
            r = Math.random() * 32767.0 / (32767.0 + 1.0);
            neighbour = (int) (r * FoodNumber);

            // 初始化解决方案
            for (j = 0; j < D; j++)
                newFood[j] = Foods[i][j];

            // 按照公式 v_{ij}=x_{ij}+\phi_{ij}*(x_{kj}-x_{ij}) 进行计算新的位置
            r = Math.random() * 32767.0 / (32767.0 + 1.0);
            newFood[param2change] = Foods[i][param2change] + (Foods[i][param2change] - Foods[neighbour][param2change]) * (r - 0.5) * 2;

            // 防止越界
            if (newFood[param2change] < lb[param2change])
                newFood[param2change] = lb[param2change];
            if (newFood[param2change] > ub[param2change])
                newFood[param2change] = ub[param2change];
            // 更新最优解
            newFoodObjection = getInput().getFunction().apply(newFood);
            newFoodFitness = CalculateFitness(newFoodObjection);

            // 在当前食物源以及新食物源之间进行贪婪选择
            if (newFoodFitness > fitness[i]) {
                // 如果新的食物源更优，那么就选它，并且重置计数器
                trial[i] = 0;
                for (j = 0; j < D; j++)
                    Foods[i][j] = newFood[j];
                f[i] = newFoodObjection;
                fitness[i] = newFoodFitness;
            } else { // 否则计数一次
                trial[i] = trial[i] + 1;
            }
        }
    }

    /**
     * 跟新跟随蜂位置
     */
    void SendOnlookerBees() {
        int i, j, t;
        i = 0;
        t = 0;
        int D = getInput().getVarNum();
        double[] lb = getInput().getLb();
        double[] ub = getInput().getUb();
        // 扫描所有食物源
        while (t < FoodNumber) {
            // 生成一个随机数，按概率进行食物源的选择
            r = Math.random() * 32767.0 / (32767.0 + 1.0);
            // 如果概率小于 prob[i]，那么寻找新的食物源（那么 t 自然也不会增加），否则就不找新的食物源（此时 t 自增）
            if (r < prob[i]) {
                t++;
                // 生成一个随机数，随机抽取一个食物源对应的自变量的位置，作为速度更新公式中的 j
                r = Math.random() * 32767.0 / (32767.0 + 1.0);
                param2change = (int) (r * D);

                // 生成一个随机数，随机抽取一个食物源，作为速度更新公式的 k
                r = Math.random() * 32767.0 / (32767.0 + 1.0);
                neighbour = (int) (r * FoodNumber);

                // 选择一个与当前方案不同的食物源
                while (neighbour == i) {
                    r = Math.random() * 32767.0 / (32767.0 + 1.0);
                    neighbour = (int) (r * FoodNumber);
                }
                for (j = 0; j < D; j++)
                    newFood[j] = Foods[i][j];

                // 按公式 v_{ij}=x_{ij}+\phi_{ij}*(x_{kj}-x_{ij}) 更新
                r = Math.random() * 32767.0 / (32767.0 + 1.0);
                newFood[param2change] = Foods[i][param2change] + (Foods[i][param2change] - Foods[neighbour][param2change]) * (r - 0.5) * 2;

                // 防止越界
                if (newFood[param2change] < lb[param2change])
                    newFood[param2change] = lb[param2change];
                if (newFood[param2change] > ub[param2change])
                    newFood[param2change] = ub[param2change];
                // 更新最优值
                newFoodObjection = getInput().getFunction().apply(newFood);
                newFoodFitness = CalculateFitness(newFoodObjection);

                // 在当前食物源以及新食物源之间进行贪婪选择
                if (newFoodFitness > fitness[i]) {
                    // 如果新的食物源更优，那么就选它，并且重置计数器
                    trial[i] = 0;
                    for (j = 0; j < D; j++)
                        Foods[i][j] = newFood[j];
                    f[i] = newFoodObjection;
                    fitness[i] = newFoodFitness;
                } else { // 否则计数一次
                    trial[i] = trial[i] + 1;
                }
            }
            // 因为不太可能每一次都寻找新的食物源，所以 t 用来控制寻找新食物源的循环，i 用来控制对于每一个食物源的循环，从而防止某个循环越界
            // 当然，i 不是终止条件，t 才是终止条件。因为这里设置要有 FoodNumber 次食物源的更新。因此可能迭代了很多遍，但是食物源更新的次数是确定的
            i++;
            if (i == FoodNumber)
                i = 0;
        }
    }

    /**
     * 锁定试验计数器超过极限值的食物来源，放弃该食物源，引领蜂转为侦察蜂，侦察一次食物源。
     * 在基本ABC中，每个周期只允许发生一次侦察
     */
    void SendScoutBees() {
        int maxtrialindex, i;
        maxtrialindex = 0;
        for (i = 1; i < FoodNumber; i++) {
            if (trial[i] > trial[maxtrialindex])
                maxtrialindex = i;
        }
        if (trial[maxtrialindex] >= limit) {
            initInIndex(maxtrialindex);
        }
    }

    /**
     * 适应度计算（根据函数值计算适应度）
     *
     * @param fun 函数值
     * @return double
     */
    double CalculateFitness(double fun) {
        double result = 0;
        if (fun >= 0) {
            result = 1 / (fun + 1);
        } else {

            result = 1 + Math.abs(fun);
        }
        return result;
    }

    /**
     * 计算食物源选择的转移概率
     */
    void CalculateProbabilities() {
        int i;
        double maxfit;
        maxfit = fitness[0];
        for (i = 1; i < FoodNumber; i++) {
            if (fitness[i] > maxfit)
                maxfit = fitness[i];
        }
        for (i = 0; i < FoodNumber; i++) {
            prob[i] = (0.9 * (fitness[i] / maxfit)) + 0.1;
        }
    }

    /**
     * 获取最优解，记录当前最好的食物来源
     */
    void MemorizeBestSource() {
        int i, j;

        for (i = 0; i < FoodNumber; i++) {
            if (f[i] < optimizedValue) {
                optimizedValue = f[i];
                for (j = 0; j < getInput().getVarNum(); j++)
                    x[j] = Foods[i][j];
            }
        }
    }
}
